using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using Moq;
using NUnit.Framework;
using OpenWaves.Caching;

namespace OpenWaves.UnitTests.Caching
{
    public abstract class ConcurrentCacheTestsBase
    {
        [Test]
        public void When_value_is_not_in_cache_GetOrCreate_creates_value()
        {
            var concurrentCache = this.CreateConcurrentCache(new Mock<ILockManager>().Object);

            var expectedValue = new object();
            var actualValue = concurrentCache.GetOrCreate("key", () => new CacheEntry<object>(expectedValue));

            Assert.That((object)actualValue, Is.SameAs(expectedValue));
        }

        [Test]
        public void When_value_is_not_in_cache_GetOrCreate_adds_value_to_cache()
        {
            var expectedValue = new object();
            var concurrentCache = this.CreateConcurrentCache(new Mock<ILockManager>().Object);

            concurrentCache.GetOrCreate("key", () => CacheEntry.FromValue(expectedValue, TimeSpan.FromHours(1)));

            Assert.That(concurrentCache.GetValue("key"), Is.SameAs(expectedValue));
        }

        [Test]
        public void When_value_is_in_cache_GetOrCreate_returns_value()
        {
            var expectedValue = new object();

            var concurrentCache = this.CreateConcurrentCache(new Mock<ILockManager>().Object);
            concurrentCache.InsertValue("key", expectedValue, TimeSpan.MaxValue);

            var actualValue = concurrentCache.GetOrCreate("key", () => null);

            Assert.That(actualValue, Is.SameAs(expectedValue));
        }

        [Test]
        public void When_value_is_in_cache_GetOrCreate_does_not_create_new_value()
        {
            var expectedValue = new object();

            var concurrentCache = this.CreateConcurrentCache(new Mock<ILockManager>().Object);
            concurrentCache.InsertValue("key", expectedValue, TimeSpan.MaxValue);

            concurrentCache.GetOrCreate("key", () =>
                                                {
                                                    Assert.Fail("This should not be called!");
                                                    return null;
                                                });
        }

        [Test]
        public void GIVEN_value_is_not_in_cache_WHEN_GetOrCreate_is_called_from_multiple_threads_THEN_value_is_created_only_once()
        {
            var concurrentCache = this.CreateConcurrentCache(new MonitorBasedLockManager());

            var threadCoordinator = new TestThreadsCoordinator();

            int counter = 0;
            Func<CacheEntry<object>> createCacheEntry = () =>
            {
                Interlocked.Increment(ref counter);

                threadCoordinator.WaitForTick(1);

                return CacheEntry.FromValue(new object(), TimeSpan.MaxValue);
            };

            threadCoordinator.RunThreads(
                () => concurrentCache.GetOrCreate("key", createCacheEntry),
                () => concurrentCache.GetOrCreate("key", createCacheEntry));

            Assert.That(counter, Is.EqualTo(1));
        }

        [Test]
        public void Given_value_is_not_in_cache_GetOrCreate_releases_lock_after_creating_new_value()
        {
            var lockManagerMock = new Mock<ILockManager>();
            var lockReleased = false;
            lockManagerMock.Setup(manager => manager.ReleaseLock("key")).Callback(() => { lockReleased = true; });

            var concurrentCache = this.CreateConcurrentCache(lockManagerMock.Object);

            concurrentCache.GetOrCreate("key", () => new CacheEntry<object>(new object()));

            Assert.That(lockReleased);
        }

        [Test]
        public void Given_value_is_not_in_cache_GetOrCreate_releases_lock_after_creating_new_value_throws()
        {
            var lockManagerMock = new Mock<ILockManager>();
            var lockReleased = false;
            lockManagerMock.Setup(manager => manager.ReleaseLock("key")).Callback(() => { lockReleased = true; });

            var concurrentCache = this.CreateConcurrentCache(lockManagerMock.Object);

            try
            {
                concurrentCache.GetOrCreate("key", () =>
                                            {
                                                throw new InvalidOperationException();
                                            });
            }
            catch
            {
                Assert.That(lockReleased);
            }
        }

        protected abstract IConcurrentCache<object> CreateConcurrentCache(ILockManager manager);

/*
        protected class SmartMonitorBasedLockManager : ILockManager
        {
            private readonly string key;


            private object @lock;

            private ConcurrentDictionary<string, KeyValuePair<object, object>> dic;

            public SmartMonitorBasedLockManager(string key)
            {
                dic=new ConcurrentDictionary<string, KeyValuePair<object, object>>();
                this.key = key;
            }

            public bool TryAcquireLock(string key, TimeSpan timeout)
            {
                var a = new Object
                var obj=dic.GetOrAdd(key, new KeyValuePair<object, object>());

                Monitor.Enter(obj);
                while (this.@lock != null)
                {
                    var timeouted= Monitor.Wait(this,timeLeft);
                    if(timeout)
                    {
                        return 
                    }
                }
                dic.TryAdd(key, obj);
                @lock=new object();
                Monitor.Exit(obj);

                if (key != this.key)
                    throw new InvalidOperationException();

                return Monitor.TryEnter(this, timeout == TimeSpan.MaxValue ? TimeSpan.FromMilliseconds(-1) : timeout);
            }

            public void ReleaseLock(string key)
            {
                Monitor.Enter(this);
                this.@lock = null;
                dic.TryRemove(key);
                Monitor.PulseAll(key);
                
                Monitor.Exit(this);


                if (key != this.key)
                    throw new InvalidOperationException();

                Monitor.Exit(this);
            }
        }
*/
    }
}